package joodle.domain;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * This is the implementation of the Shape class which contains basic implementations to the 
 * required operations of a shape. It also holds the points of the shape in an array and the 
 * attributes of the shape. It sets the attributes of the shape to default values and instantiates
 * the points array which every shape will need. This class is a blueprint for other shapes. 
 * Subshapes(rectangle, circle) will be modifying these methods in order to satisfy the requirements 
 * expected from a subshape. It also holds the unified properties of subshapes.
 */

public abstract class AbstractShape implements Shape {
	protected static final Color	DEFAULT_COLOR = Color.black;
	protected static final Color	DEFAULT_FILL_COLOR = Color.white;
	protected static final Stroke DEFAULT_STROKE = new BasicStroke(2.0f);
	
	protected List<Point> points;
	protected boolean isSelected;
	protected Path2D path;
	protected Map<Attribute, Object> attributes;
	protected AffineTransform currentTransform;
	
	public AbstractShape() {
		points = new ArrayList<Point>();
		attributes = new EnumMap<Attribute, Object>(Attribute.class);
		attributes.put(Attribute.COLOR, DEFAULT_COLOR);
		// transparent by default
		attributes.put(Attribute.FILL_COLOR, null);
		attributes.put(Attribute.LINE_STROKE, DEFAULT_STROKE);
		isSelected = false;
		currentTransform = new AffineTransform();
	}

	protected AbstractShape(final AbstractShape as) {
		path = (Path2D) as.path.clone();
		points = new ArrayList<Point>(as.points);
		// for(Point p : as.points)
		// addPoint(p); // sets awtShape
		attributes = new EnumMap<Attribute, Object>(as.attributes);
		isSelected = false;
		currentTransform = new AffineTransform(as.currentTransform);
	}
	
	/**
	 * Adds an additional point to the abstract shape then updates the shape
	 * @param p the point that will be added to the shape
	 * @see Shape#addPoint(Point)
	 */
	@Override
	public void addPoint(Point p) {
		points.add(p);
		if(hasEnoughPoints() && path == null) {
			remakeAwtShape();
		}
	}

	/**
	 * Sets the isSelected variable to the value of b
	 * @param b the value to be assigned to isSelected variable
	 * @see Shape#setSelected()
	 */
	@Override
	public void setSelected(boolean b) {
		isSelected = b;
	}

	/**
	 * Updates the current at object with the parameter at object
	 * @param at the AT object that will be concatenated to the current AT object
	 * @see Shape#transform(AffineTransform at)
	 */
	@Override
	public void transform(AffineTransform at) {
		currentTransform.preConcatenate(at);
		//path = new Path2D.Float(path, at);
	}

	/**
	 * Draws a shape with properties given in Attribute enumeration
	 * @param g the object that will be drawn
	 * @see Shape#draw(Graphics g)
	 */
	@Override
	public void draw(Graphics g) {
		if(path == null)
			return;
		
		Graphics2D g2 = (Graphics2D) g.create();
		
		g2.transform(currentTransform);

		applyAttribute(Attribute.LINE_STROKE, g2);
		
		if(applyAttribute(Attribute.FILL_COLOR, g2))
			g2.fill(path);
		
		if(applyAttribute(Attribute.COLOR, g2))
			g2.draw(path);

		if (this.isSelected()) {
			BasicStroke bs = (BasicStroke) getAttributeValue(Attribute.LINE_STROKE);
			g2.setColor(Canvas.SELECTION_COLOR);
			bs = new BasicStroke(bs.getLineWidth() + 4, BasicStroke.CAP_ROUND,
					BasicStroke.JOIN_ROUND, 4.0f, new float[] { 3.0f, 3.0f },
					0.0f);
			g2.setStroke(bs);
			g2.draw(path);
		}

		g2.dispose();
	}
	
	/**
	 * Updates the points array, changing the old point with the new one
	 * @param oldPoint the point that will be changed
	 * @param newPoint the point that will take the place of the old one
	 * @see Shape#updatePoint(Point oldPoint, Point newPoint)
	 */
	@Override
	public void updatePoint(Point oldPoint, Point newPoint) {
		for (int i = 0; i < points.size(); ++i) {
			if (points.get(i).equals(oldPoint))
				points.set(i, newPoint);
		}
	}

	/**
	 * Checks if the specified point is on the shape 
	 * @param p the point that will be checked if it's on the shape
	 * @see Shape#isPointOnShape(Point oldPoint, Point newPoint)
	 * @return a boolean value depends on the position of the specified point
	 */
	@Override
	public boolean isPointOnShape(Point p) {
		if(path == null)
			return false;
		
		return transformedBoundingBox().contains(p.awtPoint());
	}

	/**
	 * Updates the end point of the shape
	 * @param p the point that which will be the last element of the points array
	 * @see Shape#updateEndPoint(Point p)
	 */
	@Override
	public void updateEndPoint(Point p) {
		if (points.size() < 1)
			return;
		
		points.set(points.size() - 1, p);
		remakeAwtShape();
	}

	/**
	 * Checks if the shape is a composite shape
	 * @see Shape#isComposite()
	 * @return false
	 */
	@Override
	public boolean isComposite() {
		return false;
	}

	/**
	 * Checks if the shape is currently selected
	 * @see Shape#isSelected()
	 * @return a boolean value corresponds to the selected shape
	 */
	@Override
	public boolean isSelected() {
		return isSelected;
	}

	/**
	 * Return a set of attributes of the shape
	 * @see Shape#getAttributeNames()
	 * @return a set of attribute names for the composite shape
	 */
	@Override
	public Set<Attribute> getAttributeNames() {
		return attributes.keySet();
	}

	/**
	 * Returns the value of the attribute specified
	 * @param a the attribute whose value will be returned
	 * @see Shape#getAttributeValue(Attribute a)
	 * @return The value of the attribute specified
	 */
	@Override
	public Object getAttributeValue(Attribute a) {
		return attributes.get(a);
	}

	/**
	 * Changes the attribute to the desired value
	 * @param a the value to be set
	 * @param v the object that will be modified
	 * @see Shape#setAttributeValue(Attribute a)
	 */
	@Override
	public void setAttributeValue(Attribute a, Object v) {
		attributes.put(a, v);
	}

	/**
	 * Returns the point on the center of the shape
	 * @see Shape#getCenter()
	 * @return a point object which is on the center of the shape
	 */
	@Override
	public Point getCenter() {
		if (path == null)
			return null;
		
		java.awt.Rectangle br = transformedAwtShape().getBounds();
		float x = (float) br.getCenterX();
		float y = (float) br.getCenterY();
		return new Point(x, y);
	}
	/**
	 * Creates a shape by transforming the current AT object to an AWT representation
	 * @return an awt version of the current AT object
	 */
	protected java.awt.Shape transformedAwtShape() {
		return new Path2D.Float(path, currentTransform);
	}
	/**
	 * returns the bounding box of the current shape object after the 
	 * applying the current transformation to it
	 * @return a awt.Shape object
	 */
	protected java.awt.Shape transformedBoundingBox() {
		return new Path2D.Float(path.getBounds2D(), currentTransform);
	}
	
	/**
	 * Applies the attribute specified to the graphics2D object
	 * @param a the value of the attribute that will be applied
	 * @param g is the object that will be modified
	 * @return a boolean value depends on the success of the operation
	 */
	private boolean applyAttribute(Attribute a, Graphics2D g) {
		Object av = getAttributeValue(a);
		
		if(av == null)
			return false;
		
		switch(a) {
		case COLOR:
		case FILL_COLOR:
			g.setColor((Color) av);
			break;
		case LINE_STROKE:
			g.setStroke((Stroke) av);
		}
		
		return true;
	}
	
	/**
	 * Creates a character sequence which is the XML representation of the attributes
	 * @return String includes XML representation of the attributes
	 */
	protected String attributesAsXML() {
		StringBuilder sb = new StringBuilder();
		
		Color c = (Color) getAttributeValue(Attribute.COLOR);
		if(c != null) {
			sb.append(" stroke=");
			sb.append(String.format("\"#%02x%02x%02x\"", c.getRed(), c.getGreen(), c.getBlue()));
		}
		
		c = (Color) getAttributeValue(Attribute.FILL_COLOR);
		sb.append(" fill=");
		if(c != null)
			sb.append(String.format("\"#%02x%02x%02x\"", c.getRed(), c.getGreen(), c.getBlue()));
		else
			sb.append("\"none\"");

		sb.append(" stroke-width=\"");
		BasicStroke s = (BasicStroke) getAttributeValue(Attribute.LINE_STROKE);
		sb.append(s.getLineWidth());
		sb.append("\" ");
		
		return sb.toString();
	}
	
	/**
	 * Creates a character sequence which is the XML representation of the current AT object
	 * @return String includes XML representation of the current AT object
	 */
	protected String transformAsXML() {
		double[] m = new double[6];
		currentTransform.getMatrix(m);
		return String.format(" transform=\"matrix(%f %f %f %f %f %f)\" ",
				m[0], m[1], m[2], m[3], m[4], m[5]);
	}
	
	protected abstract void remakeAwtShape();
}
